﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
using Tao.FreeGlut;
using Tao.OpenGl;
using Tarea2Raytracing;
using SceneLib;
using SplineLib;
using OpenGL;

namespace Tarea3
{
    class Program
    {
        private static int WIDTH = 800;
        private static int HEIGHT = 600;
        private static float CAM_SPEED = 1.0f;
        private static float TURN_SPEED = 0.1f;

        private static Scene scene;
        private static SplineInfo spline;
        private static int program;

        private static int mode = 1;
        private static float sample = 0.15f;

        private static float h = 0.9f;
        private static float r = 15.0f;
        private static int passes = 5;

        private static int frameCount = 0;
        private static float fps = 0;
        private static int displayListRoad, displayListTerrain;

        static void Main(string[] args)
        {
            scene = new Scene(WIDTH, HEIGHT);
            scene.Load("output.xml");

            spline = new SplineInfo("Data/mycurve.sp", 0.5f);
            spline.Load();
            
            InitVariables();

            Glut.glutInit();
            Glut.glutInitDisplayMode(Glut.GLUT_RGBA | Glut.GLUT_DEPTH | Glut.GLUT_DOUBLE);
            Glut.glutInitWindowSize(WIDTH, HEIGHT);
            Glut.glutInitWindowPosition(100, 100);
            Glut.glutCreateWindow("Tarea3");

            Init();

            Glut.glutDisplayFunc(new Glut.DisplayCallback(Draw)); //Callback de dibujo
            Glut.glutKeyboardFunc(new Glut.KeyboardCallback(KeyboardHandle)); //Callback de manejo de eventos de teclado
            Glut.glutReshapeFunc(new Glut.ReshapeCallback(Resize)); //Callback de cambio de tamaño de ventana
            Glut.glutIdleFunc(new Glut.IdleCallback(Update)); //(1) Callback de función Idle
            
            Glut.glutMainLoop();
        }

        private static float previousTime;
        private static float previousTime2;
        private static Vector previousBiNormal;
        private static float radius;
        private static Vector center;
        private static Road road;
        private static Vehicle vehicle;
        private static SkyBox skybox;
        private static TerrainGenerator terrain;
        private static Ecosystem ecosystem;
        private static float hMax;

        private static void InitVariables()
        {
            float radiusx = Math.Abs(spline.XMax - spline.XMin) / 2.0f;
            float radiusz = Math.Abs(spline.ZMax - spline.ZMin) / 2.0f;
            float t = 0.0f;
            Vector normal;
            Vector tangent = spline.SplineFunctionPrime(t);

            tangent.Normalize3();
            normal = Vector.Cross3(Vector.Cross3(spline.SplineFunctionPrime(t), spline.SplineFunctionDoublePrime(t)),
                                   spline.SplineFunctionPrime(t));
            normal.Normalize3();

            radius = radiusx > radiusz ? radiusx : radiusz;
            center = new Vector(spline.XMax + spline.XMin,
                                spline.YMax + spline.YMin,
                                spline.ZMax + spline.ZMin);

            center = 0.5f * center;
            radius *= 15.0f;
            previousBiNormal = Vector.Cross3(tangent, normal);
            previousBiNormal.Normalize3();
            previousTime = 0;
            previousTime2 = 0;
            scene.Lights[0].Position = new Vector(center.x, spline.YMax * 24.5f, center.z);            

            road = new Road(scene.GetMaterial("Metal"), (SceneModel)scene.Objects[0], spline, sample);
            vehicle = new Vehicle((SceneModel)scene.Objects[1]);
            skybox = new SkyBox(scene.GetMaterial("Ice"), spline);
            terrain = new TerrainGenerator(h, r, passes, scene.GetMaterial("Stone"));
            ecosystem = new Ecosystem((SceneModel)scene.Objects[2], center, radius);
            hMax = spline.YMax * 20.0f;
        }

        //Método que agrupa inicializaciones necesarias para OpenGL
        static void Init()
        {
            Gl.glEnable(Gl.GL_DEPTH_TEST);

            Gl.glEnable(Gl.GL_LIGHTING);
            Gl.glEnable(Gl.GL_LIGHT0);
            Gl.glEnable(Gl.GL_LIGHT1);
            Gl.glEnable(Gl.GL_LIGHT2);
            Gl.glEnable(Gl.GL_LIGHT3);
            Gl.glEnable(Gl.GL_LIGHT4);
            Gl.glEnable(Gl.GL_LIGHT5);
            Gl.glEnable(Gl.GL_LIGHT6);
            Gl.glEnable(Gl.GL_LIGHT7);

            Gl.glEnable(Gl.GL_COLOR_MATERIAL);
            Gl.glColorMaterial(Gl.GL_FRONT_AND_BACK, Gl.GL_AMBIENT_AND_DIFFUSE);
            Gl.glEnable(Gl.GL_NORMALIZE);
            Gl.glEnable(Gl.GL_SMOOTH);           

            SetShaders("Shaders/Shader");
           
            displayListRoad = Gl.glGenLists(3);
            Gl.glNewList(displayListRoad, Gl.GL_COMPILE);
            road.Draw();
            Gl.glEndList();

            displayListTerrain = Gl.glGenLists(3);
            Gl.glNewList(displayListTerrain, Gl.GL_COMPILE);
            terrain.Draw(center.x - radius * 25.0f / 10.0f,
                        center.z - radius * 25.0f / 10.0f,
                        center.x + radius * 25.0f / 10.0f,
                        center.z + radius * 25.0f / 10.0f, center.y - radius);
            Gl.glEndList();
            ecosystem.InitBird();
        }

        static void SetShaders(String shader)
        {
            int vs = Gl.glCreateShader(Gl.GL_VERTEX_SHADER);
            String[] vsSource = new String[] { System.IO.File.ReadAllText(shader + ".vert") };
            Gl.glShaderSource(vs, 1, vsSource, null);
            Gl.glCompileShader(vs);

            int fs = Gl.glCreateShader(Gl.GL_FRAGMENT_SHADER);
            String[] fsSource = new String[] { System.IO.File.ReadAllText(shader + ".frag") };
            Gl.glShaderSource(fs, 1, fsSource, null);
            Gl.glCompileShader(fs);

            program = Gl.glCreateProgram();
            Gl.glAttachShader(program, vs);
            Gl.glAttachShader(program, fs);
            Gl.glLinkProgram(program);

            //Gl.glUseProgram(program);
        }


        private static float time = 0.0f;
        
        //Función para ejecutar Idle Callback
        static void Update()
        {
            float currentTime = Glut.glutGet(Glut.GLUT_ELAPSED_TIME); //ms
            float deltaTime = (currentTime - previousTime);
            float deltaTime2 = (currentTime - previousTime2);

            frameCount++;

            if (deltaTime2 > 1000)
            {
                fps = frameCount / (deltaTime2 / 1000.0f);
                Glut.glutSetWindowTitle("Tarea3, fps: " + fps.ToString());
                previousTime2 = currentTime;
                frameCount = 0;                
            }

            //Animacion a frame rate fijo: 60 FPS
            if (deltaTime > 1000.0f / 60.0f)
            {
                Vector point;
                Vector tangent, normal, binormal;

                previousTime = currentTime;                
                point = spline.SplineFunction(time);

                tangent = spline.SplineFunctionPrime(time);
                tangent.Normalize3();
                normal = Vector.Cross3(previousBiNormal, tangent);
                normal.Normalize3();
                binormal = Vector.Cross3(tangent, normal);
                binormal.Normalize3();
                previousBiNormal = binormal.Copy();

                vehicle.Rotation(tangent, -1.0f * normal, binormal);
                vehicle.Translation(point);

                time += 0.0025f * (float)Math.Sqrt(2 * 9.8 * (hMax - point.y)) / spline.SplineFunctionPrime(time).Magnitude3();

                if (mode == 1)
                {
                    scene.Camera.Position = point - 1.5f * normal - 2.5f * tangent;
                    scene.Camera.Up = -1.0f * normal;
                    scene.Camera.Target = point + 2.0f * tangent;
                }
                else if (mode == 2)
                {
                    float s = 0.5f * currentTime / 1000.0f;
                    Vector p = SplineInfo.Circle(s, radius, 0.9f * radius * (float)Math.Sin(0.1 * Math.PI * s));
                    Vector p2 = spline.SplineFunction(time * 2.0f);
                    
                    scene.Camera.Position = p;
                    scene.Camera.Up = new Vector(0, 1.0f, 0);
                    scene.Camera.Target = p2 + 5.0f * tangent;
                }

                ecosystem.Move();
            }

            Glut.glutPostRedisplay();
        }

        //Callback para manejar modificación del tamaño de la ventana
        static void Resize(int w, int h)
        {
            // Actualizamos el tamaño del viewport
            scene.Width = w;
            scene.Height = h;
            Gl.glViewport(0, 0, w, h);

            // Vamos a actualizar la matriz de proyección
            Gl.glMatrixMode(Gl.GL_PROJECTION);
            Gl.glLoadIdentity();
            Glu.gluPerspective(scene.Camera.FieldOfView,              // Ángulo FOV (se mantiene igual)
                               (double)w / (double)h,                // Razón Ancho:Alto (CAMBIÓ)
                               scene.Camera.NearClip,                // z Near (se mantiene igual)
                               scene.Camera.FarClip);                // z Far (se mantiene igual)

        }

        //Callback de display, acá hacemos todo el código de dibujo
        static void Draw()
        {
            Gl.glClearColor(scene.Background.Color.x, scene.Background.Color.y, scene.Background.Color.z, 0);
            Gl.glClear(Gl.GL_COLOR_BUFFER_BIT | Gl.GL_DEPTH_BUFFER_BIT);

            SetLights();

            Gl.glViewport(0, 0, scene.Width, scene.Height);

            Gl.glMatrixMode(Gl.GL_PROJECTION);
            Gl.glLoadIdentity();
            Glu.gluPerspective(scene.Camera.FieldOfView,
                               (double)scene.Width / (double)scene.Height,
                               scene.Camera.NearClip,
                               scene.Camera.FarClip);

            Gl.glMatrixMode(Gl.GL_MODELVIEW);
            Gl.glLoadIdentity();
            Glu.gluLookAt(scene.Camera.Position.x, scene.Camera.Position.y, scene.Camera.Position.z,
                          scene.Camera.Target.x, scene.Camera.Target.y, scene.Camera.Target.z,
                          scene.Camera.Up.x, scene.Camera.Up.y, scene.Camera.Up.z);

            Gl.glCallList(displayListRoad); //Madera

            vehicle.Draw(); //Normales modelo
            skybox.Draw(); // Ya esta
            ecosystem.Draw(); // Revisar normales

            Gl.glCallList(displayListTerrain);

            Glut.glutSwapBuffers();
        }

        private static void SetLights()
        {
            //Seteamos las luces de la escena, maximo 8 por limitación de OpenGL
            int numLights = scene.Lights.Count;
            for (int n = 0; n < numLights && n < 8; n++)
            {
                SceneLight sceneLight = scene.Lights[n];
                Gl.glLightfv(Gl.GL_LIGHT0 + n, Gl.GL_POSITION, MathUtils.GetVector3Array(sceneLight.Position)); //Posición de la luz
                Gl.glLightfv(Gl.GL_LIGHT0 + n, Gl.GL_DIFFUSE, MathUtils.GetVector4Array(sceneLight.Color)); //Color difuso
                Gl.glLightfv(Gl.GL_LIGHT0 + n, Gl.GL_SPECULAR, MathUtils.GetVector4Array(sceneLight.Color)); //Color especular (en nuestro modelo solo ocupamos un color)
                Gl.glLightf(Gl.GL_LIGHT0 + n, Gl.GL_CONSTANT_ATTENUATION, sceneLight.AtenuationConstant); //Factor de atenuación cte
                Gl.glLightf(Gl.GL_LIGHT0 + n, Gl.GL_LINEAR_ATTENUATION, sceneLight.AtenuationLinear); //Factor de atenuación lineal
                Gl.glLightf(Gl.GL_LIGHT0 + n, Gl.GL_QUADRATIC_ATTENUATION, sceneLight.AtenuationQuadratic); //Factor de atenuación cuadrático

            }

            //Definicion de color ambiente para modelo de shading
            Gl.glLightModelfv(Gl.GL_LIGHT_MODEL_AMBIENT,
                              MathUtils.GetVector3Array(scene.Background.AmbientLight));
        }

        //Handler de eventos de teclado
        static void KeyboardHandle(byte key, int x, int y)
        {
            SceneCamera sceneCam = scene.Camera;
            Vector dir = sceneCam.Target - sceneCam.Position;
            float tar_posDist = dir.Magnitude3();
            dir.Normalize3();
            Vector right = Vector.Cross3(dir, sceneCam.Up);
            Vector tempVec;

            if (key == (byte)'c')
            {
                mode = mode == 1 ? 2 : 1;
            }
            else if (key == (byte)'x')
            {
                mode = 3;
            } else if (key == (byte)'1')
            {
                Gl.glUseProgram(program);
                Glut.glutPostRedisplay();
            }
            else if (key == (byte)'2')
            {
                Gl.glUseProgram(0);
                Glut.glutPostRedisplay();
            } else if (key == (byte)'w')
             {
                 scene.Camera.Position += dir * CAM_SPEED;
                 scene.Camera.Target += dir * CAM_SPEED;
                 Glut.glutPostRedisplay();
             }
             else if (key == (byte)'s')
             {
                 scene.Camera.Position -= dir * CAM_SPEED;
                 scene.Camera.Target -= dir * CAM_SPEED;
                 Glut.glutPostRedisplay();
             }
             else if (key == (byte)'a')
             {
                 scene.Camera.Position -= right * CAM_SPEED;
                 scene.Camera.Target -= right * CAM_SPEED;
                 Glut.glutPostRedisplay();
             }
             else if (key == (byte)'d')
             {
                 scene.Camera.Position += right * CAM_SPEED;
                 scene.Camera.Target += right * CAM_SPEED;
                 Glut.glutPostRedisplay();
             }
             else if (key == (byte)'i')
             {
                 tempVec = dir + scene.Camera.Up * TURN_SPEED;
                 tempVec.Normalize3();
                 scene.Camera.Target = scene.Camera.Position + tempVec * tar_posDist;
                 scene.Camera.Up = Vector.Cross3(right, tempVec);
                 Glut.glutPostRedisplay();
             }
             else if (key == (byte)'k')
             {
                 tempVec = dir - scene.Camera.Up * TURN_SPEED;
                 tempVec.Normalize3();
                 scene.Camera.Target = scene.Camera.Position + tempVec * tar_posDist;
                 scene.Camera.Up = Vector.Cross3(right, tempVec);
                 Glut.glutPostRedisplay();
             }
             else if (key == (byte)'j')
             {
                 tempVec = dir - right * TURN_SPEED;
                 tempVec.Normalize3();
                 scene.Camera.Target = scene.Camera.Position + tempVec * tar_posDist;
                 Glut.glutPostRedisplay();
             }
             else if (key == (byte)'l')
             {
                 tempVec = dir + right * TURN_SPEED;
                 tempVec.Normalize3();
                 scene.Camera.Target = scene.Camera.Position + tempVec * tar_posDist;
                 Glut.glutPostRedisplay();
             }
        }
    }
}
